package pkg.andru.util;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import pkg.andru.collect.NonBlockingQueue;
import android.app.Application;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.os.Process;

/**
 * Like the {@link Log} class, but have some enhances: </br>a.can using tag or
 * no tag, if no tag using the line+classname as the tag, example:
 * 'L20,MyClass'. </br>b.can define log show level, if smaller than this the log
 * information will hide. </br>c.have a sign can control show all log
 * information or not. </p>the log method has 4 types: </br>x(msg) and
 * x(msg,throw): use line+class name as the default tag</br>x(tag,msg) and
 * x(tag,msg,throws): as same as Log.x(tag,msg), Log.x(tag,msg,throws)</p>
 * use:first use {@link #init(Context)} to init the config, and then
 * {@link #setMinimumLogLevel(int)} to set the display log mininum level. Also,
 * you can use {@link #setLogVisibility(boolean)} to hide or display the log.
 * </p> <b> the andru lib all classes default log level is debug, and the Lg
 * default level is info, so all library's log msg will hide except the warn and
 * error level.</b>
 * 
 * @author idiottiger
 * @version 1.0
 */
public class Log {

    public static final int VERBOSE = android.util.Log.VERBOSE;

    public static final int DEBUG = android.util.Log.DEBUG;

    public static final int INFO = android.util.Log.INFO;

    public static final int WARN = android.util.Log.WARN;

    public static final int ERROR = android.util.Log.ERROR;

    static final String EXCEPTION_TAG = "EXCEPTION";

    static boolean isShown = true, isBackup = false;

    static LoggerPrint print = new LoggerPrint();

    static final int DEFAULT_MAX_BUFFER_LOG_LINES = 200;

    static int minimumLogLevel = android.util.Log.DEBUG;

    static String packageName = "pkg.andru.util";

    static String mLogFilePath;

    static String mLogFileName;

    static int mMaxBufferLogLines = DEFAULT_MAX_BUFFER_LOG_LINES;

    private Log() {

    }

    /**
     * init the log config.<br>
     * in the manifest.xml, the application atrribute 'android:debuggable', set
     * to true will display the log, also you can change the display dynamic use
     * {@link #setLogVisibility(boolean)}.</br>suggestion inovke in the
     * {@link Application#onCreate()} method, just init startup and once.
     * 
     * @param context
     */
    public static void init(Context context) {
        try {
            packageName = context.getPackageName();
            PackageInfo pinfo = context.getPackageManager().getPackageInfo(packageName, 0);
            mLogFileName = "log_" + pinfo.versionName + "_" + pinfo.versionCode + ".log";

            final int flags = context.getPackageManager().getApplicationInfo(packageName, 0).flags;
            isShown = (flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
        } catch (Exception e) {
            isShown = false;
        }
    }

    /**
     * set the log display level, it's value must between {@link Log#VERBOSE}
     * and {@link Log#ERROR}.<br>
     * if the log level smaller than current mininum log level, will hide the
     * log.
     * 
     * @param level
     * @throws IllegalArgumentException
     *             if the level smaller than {@link Log#VERBOSE} or bigger than
     *             {@link Log#ERROR}.
     */
    public static void setMinimumLogLevel(int level) {
        if (level < Log.VERBOSE || level > ERROR) {
            throw new IllegalArgumentException("log level is:" + level + ",must between " + Log.VERBOSE + " and " + Log.ERROR);
        }
        minimumLogLevel = level;
    }

    /**
     * set log backup, if backup, you need pass the savePath, otherwise you can
     * pass null
     * 
     * @param backup
     *            backup or not
     * @param folderPath
     *            the log saved path
     * @param maxBufferLogLines
     *            max log buffer lines
     */
    public static void configLogBackupSetting(boolean backup, String folderPath, int maxBufferLogLines) {
        isBackup = backup;
        if (isBackup && folderPath != null) {
            mLogFilePath = folderPath;
            mMaxBufferLogLines = maxBufferLogLines;
            Log.i("LOG POLICY", "------------------------------- APP START, LOG START--------------------------):\n");
        }
    }

    /**
     * when app exit, you need ensure all log back up to file, if you dont use
     * {@link #configLogBackupSetting(boolean, String, int)} to config backup,
     * this method do nothing
     */
    public static void ensureBackupAllLogs() {
        if (isBackup) {
            Log.i("LOG POLICY", "------------------------------- APP END, LOG END-------------------------- (: \n");
            print.saveAllLogs();
        }
    }

    /**
     * set the log display or not.
     * 
     * @param isShow
     */
    public static void setLogVisibility(boolean isShow) {
        isShown = isShow;
    }

    public static int v(Object obj) {
        return log(Log.VERBOSE, null, obj, null);
    }

    public static int v(String tag, Object obj) {
        return log(Log.VERBOSE, tag, obj, null);
    }

    static int v(Object obj, Throwable throwable) {
        return log(Log.VERBOSE, null, obj, throwable);
    }

    public static int v(String tag, Object obj, Throwable throwable) {
        return log(Log.VERBOSE, tag, obj, throwable);
    }

    public static int d(Object obj) {
        return log(Log.DEBUG, null, obj, null);
    }

    public static int d(String tag, Object obj) {
        return log(Log.DEBUG, tag, obj, null);
    }

    static int d(Object obj, Throwable throwable) {
        return log(Log.DEBUG, null, obj, throwable);
    }

    public static int d(String tag, Object obj, Throwable throwable) {
        return log(Log.DEBUG, tag, obj, throwable);
    }

    public static int i(Object obj) {
        return log(Log.INFO, null, obj, null);
    }

    public static int i(String tag, Object obj) {
        return log(Log.INFO, tag, obj, null);
    }

    static int i(Object obj, Throwable throwable) {
        return log(Log.INFO, null, obj, throwable);
    }

    public static int i(String tag, Object obj, Throwable throwable) {
        return log(Log.INFO, tag, obj, throwable);
    }

    public static int w(Object obj) {
        return log(Log.WARN, null, obj, null);
    }

    public static int w(String tag, Object obj) {
        return log(Log.WARN, tag, obj, null);
    }

    static int w(Object obj, Throwable throwable) {
        return log(Log.WARN, null, obj, throwable);
    }

    public static int w(String tag, Object obj, Throwable throwable) {
        return log(Log.WARN, tag, obj, throwable);
    }

    public static int e(Object obj) {
        return log(Log.ERROR, null, obj, null);
    }

    public static int e(String tag, Object obj) {
        return log(Log.ERROR, tag, obj, null);
    }

    static int e(Object obj, Throwable throwable) {
        return log(Log.ERROR, null, obj, throwable);
    }

    public static int e(String tag, Object obj, Throwable throwable) {
        return log(Log.ERROR, tag, obj, throwable);
    }

    public static int printStackTrace(Throwable t) {
        return log(Log.ERROR, EXCEPTION_TAG, "exception occur:", t);
    }

    private static int log(int priority, String tag, Object msg, Throwable throwable) {
        if (priority < minimumLogLevel)
            return 0;
        String message = ((msg == null) ? "LOG NULL" : msg.toString()) + ((throwable == null) ? "" : ('\n' + android.util.Log.getStackTraceString(throwable)));
        return print.println(priority, tag, message);
    }

    /** Default implementation logs to android.util.Log */
    public static class LoggerPrint {

        static final String LEVEL_V = "V", LEVEL_D = "D", LEVEL_I = "I", LEVEL_W = "W", LEVEL_E = "E";
        static final int DEFAULT_BUFFER_SIZE = 80, OUTPUT_STREAM_BUFFER_SIZE = 8 * 1024;

        final Calendar mCalendar = new GregorianCalendar();
        final SimpleDateFormat mDateFormat = new SimpleDateFormat("MM-dd HH:mm:ss.SSS:");
        final String mLogFormat = "%s %s/%s:  %s";
        StringBuilder mLogBuffer = null;
        int mBufferLogLines;

        final Object mLocker = new Object();

        private LogFileWriter mLogFileWriter;

        public LoggerPrint() {
            mDateFormat.setCalendar(mCalendar);
            mLogBuffer = new StringBuilder(DEFAULT_BUFFER_SIZE);
            mBufferLogLines = 0;
        }

        public int println(int priority, String tag, String msg) {
            int result = 0;
            if (isBackup || isShown) {
                String _tag = getTag(tag);
                String _msg = processMessage(msg);
                if (isBackup) {
                    addLogToBuffer(priority, _tag, _msg);
                }
                if (isShown) {
                    result = android.util.Log.println(priority, _tag, _msg);
                }
            }
            return result;
        }

        protected String processMessage(String msg) {
            return String.format("%s", msg);
        }

        protected String getTag(String tag) {
            String newtag = null;
            if (tag == null) {
                final StackTraceElement trace = Thread.currentThread().getStackTrace()[6];
                newtag = "L" + trace.getLineNumber() + "," + trace.getClassName();
            } else {
                newtag = tag;
            }
            return newtag;
        }

        private void addLogToBuffer(int prior, String tag, String msg) {
            Date date = new Date(System.currentTimeMillis());
            String level = "";
            switch (prior) {
            case Log.VERBOSE:
                level = LEVEL_V;
                break;
            case Log.DEBUG:
                level = LEVEL_V;
                break;
            case Log.INFO:
                level = LEVEL_I;
                break;
            case Log.WARN:
                level = LEVEL_W;
                break;
            case Log.ERROR:
                level = LEVEL_E;
                break;
            }

            String log = String.format(mLogFormat, mDateFormat.format(date), level, tag, msg);
            if (log != null) {
                synchronized (mLogBuffer) {
                    if (mBufferLogLines > mMaxBufferLogLines) {
                        String bigString = mLogBuffer.toString();
                        backupLogToFile(bigString);
                        resetLogBuffer();
                    }
                    mLogBuffer.append(log);
                    mLogBuffer.append("\n");
                    mBufferLogLines++;
                }
            }
        }

        private void resetLogBuffer() {
            mLogBuffer = null;
            mLogBuffer = new StringBuilder();
            mBufferLogLines = 0;
        }

        private void backupLogToFile(final String logs) {
            if (mLogFileWriter == null) {
                mLogFileWriter = new LogFileWriter();
                mLogFileWriter.start();
            }
            mLogFileWriter.putLogToWriteQueue(logs);
        }

        public void saveAllLogs() {
            String restLog = null;
            if (mLogBuffer != null) {
                restLog = mLogBuffer.toString();
                resetLogBuffer();
                if (mLogFileWriter == null) {
                    mLogFileWriter = new LogFileWriter();
                    mLogFileWriter.start();
                }
                if (restLog != null) {
                    mLogFileWriter.putLogToWriteQueue(restLog);
                }
            }
            if (mLogFileWriter != null) {
                mLogFileWriter.makeCanQuit();
                try {
                    mLogFileWriter.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                mLogFileWriter = null;
            }
        }

        class LogFileWriter extends Thread {

            NonBlockingQueue<String> mLogWriteQueue = null;

            volatile boolean isClosed;

            public LogFileWriter() {
                mLogWriteQueue = new NonBlockingQueue<String>(10);
                mLogWriteQueue.clear();
            }

            @Override
            public synchronized void start() {
                super.start();
            }

            @Override
            public void run() {
                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                setName("log saver");
                while (true) {
                    if (mLogWriteQueue.isEmpty()) {
                        if (isClosed) {
                            break;
                        }
                        synchronized (mLogWriteQueue) {
                            try {
                                mLogWriteQueue.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                    String log = mLogWriteQueue.pop();
                    if (log != null) {
                        writeLog(log);
                    }
                }
            }

            public void putLogToWriteQueue(String logs) {
                if (!isClosed) {
                    synchronized (mLogWriteQueue) {
                        mLogWriteQueue.push(logs);
                        mLogWriteQueue.notify();
                    }
                }
            }

            public void wakeUp() {
                synchronized (mLogWriteQueue) {
                    mLogWriteQueue.notify();
                }
            }

            public void makeCanQuit() {
                isClosed = true;
                wakeUp();
            }

            void writeLog(String log) {
                File file = new File(mLogFilePath);
                if (!file.exists()) {
                    file.mkdirs();
                }
                if (file.exists()) {
                    File logFile = new File(file, mLogFileName);
                    BufferedOutputStream bos = null;
                    try {
                        bos = new BufferedOutputStream(new FileOutputStream(logFile, true), OUTPUT_STREAM_BUFFER_SIZE);
                        byte[] bytes = log.getBytes();
                        int size = bytes.length;
                        bos.write(bytes, 0, size);
                        bos.flush();
                        bos.close();
                    } catch (Exception e) {
                        Log.printStackTrace(e);
                    } finally {
                        if (bos != null) {
                            try {
                                bos.close();
                            } catch (IOException e) {
                            }
                        }
                    }
                }
            }
        }

    }

}
